﻿//++
//
// Copyright (c) Microsoft Corporation.  All rights reserved.
//
// Module Name:
//
//    AddEditNotificationRuleDialog.cs
//
// Abstract:
//    
//    The add/edit rule dialog.
//
//--

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Security.Permissions;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using Microsoft.OfficeCommunicationsServer.Applications.Common;

namespace Microsoft.EnhancedReminders.OutlookAddIn
{
    /// <summary>
    /// Interaction logic for AddEditNotificationRuleDialog.xaml
    /// </summary>
    public partial class AddEditNotificationRuleDialog : Window
    {
        /// <summary>
        /// Command for CheckBox events.
        /// </summary>
        public readonly static RoutedCommand CheckBoxCommand = new RoutedCommand();

        /// <summary>
        /// Command for "Back" button events.
        /// </summary>
        public readonly static RoutedCommand BackCommand = new RoutedCommand();
        
        /// <summary>
        /// Command for "Next" button events.
        /// </summary>
        public readonly static RoutedCommand NextCommand = new RoutedCommand();

        /// <summary>
        /// Command for "Finish" button events.
        /// </summary>
        public readonly static RoutedCommand FinishCommand = new RoutedCommand();

        private NotificationRuleItem _updatedRule;
        private List<ConditionData> _conditions;

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="rule">The rule to edit or null to create a new rule</param>
        /// <param name="showActionPage">True to display the actions (rather than the conditions) page at startup.</param>
        [EnvironmentPermissionAttribute(SecurityAction.LinkDemand, Unrestricted = true)]
        public AddEditNotificationRuleDialog(NotificationRuleItem rule, bool showActionPage)
            : this(rule, showActionPage, new OutlookApplication(Globals.ThisAddIn.Application))
        { 
        }

        /// <summary>
        /// Constructor.  This constructor instance is called directly by unit test code to pass in a mock channel list.
        /// </summary>
        /// <param name="rule">The rule to edit or null to create a new rule</param>
        /// <param name="showActionPage">True to display the actions (rather than the conditions) page at startup.</param>
        /// <param name="application">The Outlook application to use (DRTs pass in a mock application instance).</param>
        [EnvironmentPermissionAttribute(SecurityAction.LinkDemand, Unrestricted = true)]
        public AddEditNotificationRuleDialog(NotificationRuleItem rule, bool showActionPage, IOutlookApplication application)
        {
            Debug.Assert(application != null, "application");

            InitializeComponent();

            // Load the master list of channels from central storage.
            List<NotificationChannel> masterChannels = ReminderOutlookUtils.LoadChannels(application, /*restoreDefaultsIfNecessary*/false);

            if (rule != null) 
            {
                // Editing a rule - create a copy of the rule which is discarded if the user cancels.
                Title = Properties.Resources.RulesDialogEditRuleTitle;
                _updatedRule = new NotificationRuleItem(rule);

                if (showActionPage) // Only valid when editing a rule.
                {
                    AddEditNotificationRuleDialog.NextCommand.Execute(this, nextButton);
                }
            }
            else 
            {
                // Adding a new rule - create a new rule instance.
                Title = Properties.Resources.RulesDialogAddRuleTitle;
                _updatedRule = new NotificationRuleItem(MailboxType.Voicemail, String.Empty, DateTime.UtcNow, masterChannels);

                Debug.Assert(_updatedRule.Conditions != null, "_updatedRule.Conditions");
                Debug.Assert(_updatedRule.Conditions.Count > 0, "_updatedRule.Conditions.Count");

                _updatedRule.Conditions[0].IsSelected = true;
            }

            // Update the channels referred to by the rule to match the current master channel list.  We need to do 
            // this here so that the "Finish" button will be correctly enabled for the current channel settings.
            // In the case of adding a new rule we also need to sync in order to remove invalid channels from the lists.
            _updatedRule.SyncToMasterChannelsList(masterChannels, /*appendUnseenChannels*/true);

            nameTextBox.Text = _updatedRule.RuleName;

            containerGrid.Background = cancelButton.Background;
          
            CreateConditions(_updatedRule);

            // Verify that the rule type combo box provides the expected choices in the expected order and with
            // each entry appropriately enabled.  The item index in the combo box list is bound to the property
            // RuleTypeSelectedIndex which maps this to the appropriate MailBoxType enum value.  This check verifies the
            // combo box items match the assumptions made by RuleTypeSelectedIndex.
            Debug.Assert(ruleTypeComboBox.Items.Count == 4, "ruleTypeComboBox.Items.Count");
            VerifyComboBoxItem(ruleTypeComboBox.Items[0] as ComboBoxItem, Properties.Resources.RulesDialogNewVoiceMails, /*isEnabled*/true);
            VerifyComboBoxItem(ruleTypeComboBox.Items[1] as ComboBoxItem, Properties.Resources.RulesDialogCalendarItems, /*isEnabled*/true);
            VerifyComboBoxItem(ruleTypeComboBox.Items[2] as ComboBoxItem, Properties.Resources.RulesDialogNewEMails, /*isEnabled*/false);
            VerifyComboBoxItem(ruleTypeComboBox.Items[3] as ComboBoxItem, Properties.Resources.RulesDialogTasks, /*isEnabled*/false);

            // Populate the voicemail and calendar channels pages.  The Join now control filters out
            // any non-join now channels and the control-owned channel lists are used to
            // determine whether the "Finish" button can be enabled.
            voicemailChannelsControl.Initialize(FilterNonVoicemailChannels(_updatedRule.NotificationChannels.Channels));
            calendarActionsControl.Initialize(_updatedRule.NotificationChannels, _updatedRule.JoinNowChannels);
        }

        /// <summary>
        /// Filters out channels that are prohibited from receiving voicemail reminders. This helps reduce the
        /// chance that we'll enter a loop where a voicemail reminder triggers another voicemail reminder.
        /// </summary>
        private static List<NotificationChannel> FilterNonVoicemailChannels(List<NotificationChannel> channels)
        {
            Debug.Assert(channels != null, "channels != null");

            // For now only the desk phone is prohibited from receiving voicemail reminders. Other
            // channels may be added to this list in the future or compared to the desk channel to make
            // sure a customer did not add a custom channel that maps to their desk phone.
            List<NotificationChannel> voiceMailChannels = new List<NotificationChannel>();
            foreach (NotificationChannel channel in channels)
            {
                if (channel.Id != NotificationChannel.WorkPhoneId)
                {
                    voiceMailChannels.Add(channel);
                }
            }

            return voiceMailChannels;
        }

        /// <summary>
        /// Verify that a combo box item for selecting the rule type is enabled correctly and displays 
        /// the appropriate text for the rule type.
        /// </summary>
        private static void VerifyComboBoxItem(ComboBoxItem item, string expectedText, bool isEnabled)
        {
            Debug.Assert(item != null, "item");
            Debug.Assert(!String.IsNullOrEmpty(expectedText), "expectedText");

            StackPanel stackPanel = Helpers.AssertCast<StackPanel>(item.Content);
            Debug.Assert(stackPanel.Children.Count == 2, "stackPanel.Children.Count");

            TextBlock text = Helpers.AssertCast<TextBlock>(stackPanel.Children[1]);

            Debug.Assert(text.Text == expectedText, "Unexpected text for ComboBoxItem");
            Debug.Assert(item.IsEnabled == isEnabled, "item.IsEnabled");
        }

        /// <summary>
        /// Create a ConditionData instance for each rule condition.
        /// </summary>
        /// <param name="rule">The rule to be added/edited.</param>
        private void CreateConditions(NotificationRuleItem rule)
        {
            Debug.Assert(rule != null, "rule");

            _conditions = new List<ConditionData>();

            NotificationCondition firstSelectedCondition = 
                NotificationConditionHelper.FindFirstSelectedCondition(rule);

            foreach (NotificationCondition condition in rule.Conditions)
            {
                ConditionData conditionData = null;
                NotificationTimeCondition timeCondition = condition as NotificationTimeCondition;
                NotificationPeopleCondition peopleCondition = condition as NotificationPeopleCondition;
                NotificationStatusCondition statusCondition = condition as NotificationStatusCondition;

                bool displayPanel = condition == firstSelectedCondition; // True to display this condition.

                if (timeCondition != null)
                {
                    conditionData = new ConditionData(timeCondition, timeCheckBox, timeTextBlock, timePanel, displayPanel);
                    timeControl.Initialize(timeCondition.TimeArray);                   
                }
                else if (peopleCondition != null)
                {
                    conditionData = new ConditionData(condition, peopleCheckBox, peopleTextBlock, peoplePanel, displayPanel);
                    peopleControl.Initialize(peopleCondition.Contacts);
                }
                else if (statusCondition != null)
                {
                    conditionData = new ConditionData(condition, statusCheckBox, statusTextBlock, statusPanel, displayPanel);
                    statusControl.Initialize(statusCondition);
                }

                Debug.Assert(conditionData != null, "Unexpected condition type");

                _conditions.Add(conditionData);
            }

            EnableConditionsForRuleType(/*switchingType*/false);

            Debug.Assert(_conditions.Count == 3, "Three conditions should have been created");
        }

        /// <summary>
        /// Index of the currently selected rule type.  This is bound to the SelectedIndex property
        /// of the rule type combo box and mapped to the appropriate MailBoxType enum value.
        /// Note that MailBoxType values are not assumed to directly map to the combo box index (to
        /// keep this type definition independent of UI presentation).
        /// </summary>
        public int RuleTypeSelectedIndex
        {
            get
            {
                Debug.Assert(_updatedRule.MailboxType == MailboxType.Voicemail ||
                             _updatedRule.MailboxType == MailboxType.CalendarItem,
                             "Unexpected MailboxType");
                
                return _updatedRule.MailboxType == MailboxType.Voicemail ? 0 : 1;
            }
            set
            {
                Debug.Assert(value == 0 || value == 1, "Only voicemail or calendar items can be selected");

                _updatedRule.MailboxType = value == 0 ? MailboxType.Voicemail : MailboxType.CalendarItem;

                EnableConditionsForRuleType(/*switchingType*/true);
            }
        }

        /// <summary>
        /// Ensure the condition text descriptions and checkboxes are enabled/checked appropriately
        /// for the selected rule type.
        /// </summary>
        /// <param name="switchingType">True if selecting a new rule type; false otherwise.</param>
        private void EnableConditionsForRuleType(bool switchingType)
        {
            // Text blocks need to be explicitly enabled as they do not take a routed command.
            peopleTextBlock.IsEnabled = _updatedRule.MailboxType == MailboxType.Voicemail;
            statusTextBlock.IsEnabled = _updatedRule.MailboxType == MailboxType.Voicemail;

            if (!switchingType)
            {
                return;
            }

            // When switching to a different rule type the time condition should be 
            // checked and displayed and all other conditions unchecked.  This overrides
            // which conditions were selected previously for this type but not the 
            // condition data (e.g. the time grid) itself.
            foreach (ConditionData conditionData in _conditions)
            {
                if (conditionData.Condition.GetType() == typeof(NotificationTimeCondition))
                {
                    conditionData.IsSelected = true;
                    ShowCondition(timeTextBlock, null);
                }
                else
                {
                    conditionData.IsSelected = false;
                }
            }
        }

        /// <summary>
        /// Command handler invoked when a condition checkbox is checked or unchecked.  If checked,
        /// this selects the condition and switches the display panel to view that condition.
        /// </summary>
        private void SelectCondition(object sender, ExecutedRoutedEventArgs e)
        {
            Debug.Assert(null != sender, "sender");
            Debug.Assert(null != e, "e");

            CheckBox checkbox = Helpers.AssertCast<CheckBox>(e.OriginalSource);

            foreach (ConditionData data in _conditions)
            {
                // Select the condition which contains the same checkbox instance.
                if (object.ReferenceEquals(checkbox, data.Checkbox))
                {
                    data.IsSelected = _updatedRule.MailboxType == MailboxType.Voicemail ? 
                        checkbox.IsChecked.Value :
                        true; // Calendar rules have only one enabled condition (time), so disallow unchecking.
                    
                    // Checking or unchecking a condition causes that condition panel to be displayed.
                    ShowCondition(data.TextBlock, /*MouseButtonEventArgs*/null);
                }
            }
        }

        /// <summary>
        /// Determines whether a condition checkbox should be enabled or not.
        /// </summary>
        private void SelectConditionCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            Debug.Assert(null != sender, "sender");
            Debug.Assert(null != e, "e");

            CheckBox checkbox = Helpers.AssertCast<CheckBox>(e.OriginalSource);

            e.CanExecute = checkbox == timeCheckBox || 
                           (_updatedRule != null && _updatedRule.MailboxType == MailboxType.Voicemail);
        }

        /// <summary>
        /// Handler to display the condition when the text description of the condition is clicked.
        /// </summary>
        /// <remarks>
        /// This is a single handler for all instances of the textblock being clicked.
        /// </remarks>
        private void ShowCondition(object sender, MouseButtonEventArgs e)
        {
            Debug.Assert(null != sender, "sender");

            foreach (ConditionData data in _conditions)
            {
                if (object.ReferenceEquals(sender, data.TextBlock))
                {
                    data.Show();
                }
                else
                {
                    data.Hide();
                }
            }
        }

        /// <summary>
        /// The updated rule which is returned to the parent when the dialog closes.
        /// </summary>
        internal NotificationRuleItem UpdatedRule
        {
            get
            {
                return _updatedRule;
            }
        }

        /// <summary>
        /// Command handler invoked when the "Back" button is clicked.
        /// </summary>
        private void ShowConditions(object sender, ExecutedRoutedEventArgs e)
        {
            Debug.Assert(null != sender, "sender");
            Debug.Assert(null != e, "e");

            voicemailActionsGrid.Visibility = Visibility.Hidden;
            calendarActionsGrid.Visibility = Visibility.Hidden;
            conditionsGrid.Visibility = Visibility.Visible;
        }

        /// <summary>
        /// True if the "Back" button should be enabled.
        /// </summary>
        private void ShowConditionsCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            // and each selected condition must be valid.
            e.CanExecute = conditionsGrid.Visibility == Visibility.Hidden;
        }

        /// <summary>
        /// Command handler invoked when the "Next>" button is clicked.
        /// </summary>
        internal void ShowActions(object sender, ExecutedRoutedEventArgs e)
        {
            Debug.Assert(null != sender, "sender");
            Debug.Assert(null != e, "e");

            conditionsGrid.Visibility = Visibility.Hidden;
           
            if (_updatedRule.MailboxType == MailboxType.Voicemail)
            {
                voicemailActionsGrid.Visibility = Visibility.Visible;
            }
            else 
            {
                Debug.Assert(_updatedRule.MailboxType == MailboxType.CalendarItem, "Expecting calendar rule");
                calendarActionsGrid.Visibility = Visibility.Visible;
            }
        }

        /// <summary>
        /// True if the "Next>" button should be enabled.
        /// </summary>
        private void ShowActionsCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            Debug.Assert(null != sender, "sender");
            Debug.Assert(null != e, "e");

            e.CanExecute = conditionsGrid.Visibility == Visibility.Visible &&
                NotificationConditionHelper.VerifyConditions(_updatedRule);
        }

        /// <summary>
        /// Command handler invoked when the "Finish" button is clicked.
        /// </summary>
        private void Finish(object sender, ExecutedRoutedEventArgs e)
        {
            Debug.Assert(null != sender, "sender");
            Debug.Assert(null != e, "e");

            if (_updatedRule.MailboxType == MailboxType.Voicemail)
            {
                _updatedRule.NotificationChannels =
                    new NotificationChannelListAbsoluteTime(voicemailChannelsControl.Channels.Channels, 
                        DateTime.MaxValue /* Ensures this object appears enabled */);
            }
            else
            {
                Debug.Assert(_updatedRule.MailboxType == MailboxType.CalendarItem, "Expected calendar rule");
                _updatedRule.NotificationChannels = calendarActionsControl.NotificationChannels;
                _updatedRule.JoinNowChannels = calendarActionsControl.JoinNowChannels;
            }

            DialogResult = true; // Notifies the parent that a rule was successfully added/edited.
            Close();
        }

        /// <summary>
        /// True if the "Finish" button should be enabled.
        /// </summary>
        private void FinishCanExecute(object sender, CanExecuteRoutedEventArgs e)
        {
            // Name textbox must be populated; at least one condition must be selected;
            // and each selected condition must be valid and at least one channel
            // must be available to send the notification on (for calendar items a join
            // now channel must be enabled if no notification channels are).
            e.CanExecute = NotificationConditionHelper.VerifyConditions(_updatedRule) &&
                ChannelsAreEnabled();
        }

        /// <summary>
        /// Returns true if a voicemail rule has at least one channel enabled OR
        /// a calendar rule has either notification or join now channels (including
        /// selection of a valid time).
        /// </summary>
        private bool ChannelsAreEnabled()
        {
            if (_updatedRule.MailboxType == MailboxType.Voicemail)
            {
                NotificationChannelList channels = new NotificationChannelList(voicemailChannelsControl.Channels.Channels);
                return channels.IsEnabled;
            }

            Debug.Assert(_updatedRule.MailboxType == MailboxType.CalendarItem, "Unexpected rule type");

            return calendarActionsControl.NotificationChannels.IsEnabled ||
                   calendarActionsControl.JoinNowChannels.IsEnabled;
        }

        /// <summary>
        /// True if rule name is updated.
        /// </summary>
        private void NameTextBox_TextChanged(object sender, TextChangedEventArgs e)
        {
            Debug.Assert(null != sender, "sender");
            Debug.Assert(null != e, "e");

            _updatedRule.RuleName = nameTextBox.Text;
        }
    }
}

